7장. 글자를 숫자로 바꾸기
출처: 『AI 엔지니어링 선수지식』(youtubedu 자체 제작 학습노트) | 입문판 재구성 — 노트가 1차 소스(PDF 원본 없음)
코드는 분위기만 — numpy 같은 말은 몰라도 됩니다. 표의 '비유'와 '위험'만 봐도 충분해요.
AI 는 글자를 직접 못 읽습니다.
컴퓨터는 숫자만 압니다.
그래서 "강아지" 같은 글자를 먼저 숫자로 바꿔 줘야 합니다.
이 장은 그 "글자를 숫자로 바꾸는 입구"를 다룹니다.
0. 이 장의 새 단어 (0장에 없는 것만)
0장 용어집에 이미 있는 말(벡터·임베딩·내적·코사인 유사도)은 그대로 가져다 씁니다.
여기서는 0장에 없던 새 단어 3개만 미리 풀어 둡니다.
토큰(token)
한 문장 뜻 — 문장을 쪼갠 의미 조각 하나. AI 가 한 번에 받아들이는 최소 단위입니다.
일상비유 — 레고 한 조각. "안녕하세요"라는 큰 덩어리를 "안녕" + "하세요" 같은 작은 조각으로 나눈 것.
한 줄 예 —
# 문장을 조각으로 나눈 결과 = 토큰 목록
tokens = ["안녕", "하세요"]
위치 표식(position)
한 문장 뜻 — 각 토큰이 문장에서 몇 번째인지 적어 두는 번호.
일상비유 — 극장 좌석 번호. 같은 사람들이 앉아도 자리가 다르면 다른 그림이 됩니다.
한 줄 예 —
# "철수=0번, 좋아해=1번, 영희=2번" 처럼 순서를 번호로 붙임
positions = [0, 1, 2]
정규화(normalize)
한 문장 뜻 — 겉보기엔 같지만 속이 다른 글자를, 하나의 표준 모양으로 통일하는 것.
일상비유 — 같은 "한" 글자를 누구는 도장 한 방으로, 누구는 자음·모음 조각으로 적습니다. 그걸 한 가지 방식으로 맞추는 일.
한 줄 예 —
# 글자를 표준 모양으로 통일
import unicodedata
clean = unicodedata.normalize("NFC", "한글")
(귀납 도입) 이런 적 있죠?
채팅창에 "강아지"라고 입력했습니다.
그런데 AI 안쪽에서는 이 글자를 그대로 쓰지 않습니다.
먼저 조각(토큰)으로 나누고, 각 조각에 번호(ID)를 붙이고, 그 번호를 의미가 담긴 숫자 줄(임베딩)로 바꿉니다.
이 줄을 한 줄로 적으면 이렇습니다.
"문장 → 토큰 → ID → 임베딩"
이 변환이 AI 가 글자를 받아들이는 입구입니다.
이 장에서는 이 입구의 네 부품을 하나씩 봅니다.
이 장에서 딱 4가지만 (TL;DR)
- 토큰화·BPE — 문장을 조각으로 쪼개고, 자주 같이 나오는 조각은 한 덩어리로 묶는다.
- 임베딩 — 토큰 번호를 "의미가 담긴 숫자 줄"로 바꾼다. 뜻이 비슷하면 방향이 비슷하다.
- 위치 표식(위치 임베딩) — 단어가 몇 번째인지 알려 준다. 순서가 바뀌면 뜻도 바뀌니까.
- NFC 정규화 — 겉보기 같은 한글을 한 가지 표준 모양으로 통일한다.
한 문장으로 — 글자를 숫자로 바꿔 AI 입에 떠먹여 주는 네 가지 준비입니다.
개념 1 — 토큰화·BPE (문장을 조각으로 쪼개기)
막히는 장면
"안녕하세요"를 AI 에 넣고 싶습니다.
그런데 통째로 한 덩어리로 주면, AI 는 이 긴 글자를 다룰 단위가 없습니다.
그렇다고 글자 하나하나("안","녕","하","세","요")로 쪼개면 조각이 너무 잘아집니다.
적당히 의미 있는 조각으로 나눠야 합니다.
그게 토큰화입니다.
일상비유 — 레고 분해
큰 레고 작품을 조각으로 분해한다고 해 봅시다.
자주 같이 붙어 다니는 조각은 굳이 낱개로 안 떼고 한 덩어리로 둡니다.
"low", "lower", "lowest" 에는 공통으로 "low" 가 들어 있죠.
이 "low" 를 매번 쪼개지 않고 한 조각으로 묶어 둡니다.
이렇게 "자주 같이 나오는 조각을 한 덩어리로 묶는 규칙"이 BPE 입니다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 레고를 조각으로 분해 | tokens = ["안녕", "하세요"] |
너무 잘게 쪼개면 조각 수가 폭증 |
| 자주 쓰는 조각은 한 덩어리 | vocab = {"안녕": 0, "하세요": 1} |
한글은 영어보다 조각이 많이 나옴 |
한 문장 정의 — 토큰화는 문장을 의미 조각(토큰)으로 쪼개는 것이고, BPE 는 자주 같이 나오는 조각을 한 덩어리로 묶는 규칙입니다.
각 토큰에는 번호(ID)를 붙입니다.
그 번호 장부가 어휘 사전입니다.
# 어휘 사전: 조각마다 번호를 매겨 둔 장부
vocab = {"안녕": 0, "하세요": 1}
ids = [vocab[t] for t in ["안녕", "하세요"]]
print(ids) # [0, 1]
예시 폭격
(1) 완성예 — 영어 "low", "lower", "lowest" 를 쪼개면
# 공통 "low" 는 한 조각으로 묶이고, 뒤만 달라짐
print(["low"]) # ['low']
print(["low", "er"]) # ['low', 'er']
print(["low", "est"]) # ['low', 'est']
"low" 가 세 단어에서 모두 같은 한 조각으로 재사용됩니다.
(2) 부분완성 — 빈칸을 채워 보세요.
# "안녕하세요" 를 두 조각으로 나누면?
tokens = ["안녕", "____"] # 빈칸: 하세요
(3) 독립적용 — "강아지가" 를 직접 조각으로 나눠 보세요.
조각 수가 적을수록 AI 가 한 번에 다루기 편합니다.
미니 시나리오 — 요금이 왜 더 나오지?
같은 내용을 영어로 쓸 때와 한글로 쓸 때, AI 사용 요금이 다릅니다.
요금은 보통 토큰 수로 매겨집니다.
한글은 영어보다 조각이 더 많이 나오는 편입니다.
그래서 같은 뜻이라도 한글이 토큰 수가 많아 비용이 더 들 수 있습니다.
더 깊이 (지금 몰라도 됨) — Hugging Face LLM Course
개념 2 — 임베딩 (번호를 의미 숫자 줄로)
막히는 장면
토큰에 번호를 붙였습니다.
"안녕=0, 하세요=1" 처럼요.
그런데 이 번호 0, 1 은 그냥 순서일 뿐, 뜻이 없습니다.
번호 0 과 1 이 가깝다고 "안녕"과 "하세요"가 뜻이 비슷한 것도 아니죠.
뜻을 담은 숫자가 필요합니다.
그래서 번호를 의미가 담긴 숫자 줄(임베딩)로 바꿉니다.
일상비유 — 의미 지도
단어마다 지도 위의 점을 하나씩 찍는다고 해 봅시다.
뜻이 비슷한 단어끼리는 가까이 모입니다.
"강아지"와 "개"는 바로 옆이고, "강아지"와 "자동차"는 멀리 떨어집니다.
이 "의미 좌표"가 임베딩입니다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 단어마다 의미 좌표 | dog = np.array([0.9, 0.1, 0.8]) |
좌표가 엉터리면 검색·추천이 헛다리 |
| 가까우면 비슷한 뜻 | dog @ car / ...(코사인 유사도) |
차원(칸 수)이 안 맞으면 비교 불가 |
한 문장 정의 — 임베딩은 토큰 번호를 의미가 담긴 숫자 벡터로 바꾼 것이고, 뜻이 비슷하면 방향(코사인 유사도)이 비슷합니다.
코사인 유사도와 벡터는 0장에 정의돼 있습니다. 두 방향이 같으면 1, 정반대면 -1 이라는 그 값입니다.
예시 폭격
(1) 완성예 — "강아지"와 "자동차"는 멀까?
import numpy as np
from numpy.linalg import norm
dog = np.array([0.9, 0.1, 0.8])
car = np.array([0.1, 0.9, 0.05])
print(round(dog @ car / (norm(dog) * norm(car)), 2)) # 0.4 (멀음)
값이 0.4 로 낮으니 둘은 뜻이 먼 단어입니다.
(2) 부분완성 — "강아지"와 "개"는 가까울까?
# 둘 다 비슷한 방향이면 값이 1 에 가깝게 나옴
# 빈칸을 1 에 가까운 좌표로 채워 보세요
dog = np.array([0.9, 0.1, 0.8])
puppy = np.array([0.88, 0.12, ____]) # 빈칸: 0.79 같은 비슷한 값
(3) 독립적용 — "사과"와 "바나나"의 좌표를 직접 비슷하게 찍어, 값이 1 에 가깝게 나오는지 확인해 보세요.
미니 시나리오 — "의미 검색"이 가능한 이유
검색창에 "강아지 키우기"라고 쳤습니다.
글자가 똑같이 들어 있는 문서만 찾으면 "반려견 입양 가이드"는 못 찾습니다.
하지만 임베딩으로 바꾸면 "강아지"와 "반려견"의 좌표가 가깝습니다.
그래서 글자가 달라도 뜻이 비슷한 문서를 찾아냅니다.
이게 "의미 검색"입니다.
더 깊이 (지금 몰라도 됨) — Jay Alammar: Illustrated Transformer
개념 3 — 위치 표식 (몇 번째인지 알려 주기)
막히는 장면
"철수가 영희를 좋아해"
"영희가 철수를 좋아해"
두 문장에 쓰인 단어는 똑같습니다. {철수, 영희, 좋아해}
다른 건 순서뿐입니다.
그런데 단어를 그냥 한 묶음으로만 주면, AI 는 누가 누구를 좋아하는지 구분 못 합니다.
순서 정보가 빠졌기 때문입니다.
그래서 각 단어에 "너는 몇 번째야"라는 위치 표식을 붙입니다.
(이 위치 표식을 숫자 줄에 새겨 넣은 것을 위치 임베딩이라고 부릅니다.)
일상비유 — 극장 좌석 번호
같은 관객 세 명이 극장에 들어갑니다.
자리에 번호가 없으면, 누가 앞줄이고 누가 뒷줄인지 알 수 없습니다.
좌석 번호를 붙이면 "철수=1번 자리, 영희=3번 자리"처럼 자리가 확정됩니다.
단어에도 똑같이 번호를 붙이는 겁니다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 좌석 번호 붙이기 | positions = [0, 1, 2] |
번호 없으면 순서를 모름 |
| 자리로 의미 확정 | 단어 + 위치를 함께 넣음 | 순서 뒤바뀌면 뜻이 정반대 |
한 문장 정의 — 위치 표식은 각 단어가 몇 번째인지 알려 주는 번호이고, 순서가 뜻을 바꾸는 문장을 AI 가 구분하게 해 줍니다.
예시 폭격
(1) 완성예 — 단어에 번호를 붙이면
words = ["철수가", "영희를", "좋아해"]
positions = [0, 1, 2]
print(list(zip(words, positions)))
# [('철수가', 0), ('영희를', 1), ('좋아해', 2)]
이제 "철수가"가 0번(맨 앞)이라는 게 확정됩니다.
(2) 부분완성 — 순서를 뒤집으면 번호는?
words = ["영희가", "철수를", "좋아해"]
positions = [0, 1, ____] # 빈칸: 2
단어 집합은 같아도, 0번이 "영희가"로 바뀌어 뜻이 달라집니다.
(3) 독립적용 — "나는 너를 봤다"의 각 단어에 0,1,2 번호를 직접 붙여 보세요.
미니 시나리오 — 긴 글도 순서를 안 잃는다
AI 에 아주 긴 글을 넣는다고 해 봅시다.
문장이 길어질수록 "이 단어가 몇 번째였지?"를 놓치기 쉽습니다.
위치 표식을 잘 설계하면, 긴 글에서도 단어 순서를 정확히 유지합니다.
덕분에 긴 글도 앞뒤 순서를 헷갈리지 않고 읽습니다.
더 깊이 (지금 몰라도 됨) — Stanford CS224N · Jay Alammar Illustrated Transformer
개념 4 — NFC 정규화 (겉보기 같은 한글 통일하기)
막히는 장면
"한글"이라고 적힌 두 파일이 있습니다.
화면에는 완전히 똑같아 보입니다.
그런데 컴퓨터로 두 글자가 같은지 비교하니 False(다르다)라고 나옵니다.
분명히 같은 글자인데요?
이유는 한글을 저장하는 방식이 두 가지라서입니다.
이 둘을 한 가지 표준 모양으로 맞추는 게 NFC 정규화입니다.
일상비유 — 같은 글자, 다른 조립
"한"이라는 글자를 적는 방법이 두 가지입니다.
하나는 완성된 "한"을 도장 한 방으로 찍는 방식(NFC).
다른 하나는 ㅎ + ㅏ + ㄴ 조각을 따로 적어 조립하는 방식(NFD).
사람 눈엔 똑같지만, 컴퓨터는 둘을 다른 글자로 봅니다.
"NFC 로 통일하자"는 건 "전부 도장 한 방 방식으로 맞추자"는 약속입니다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 도장 한 방(NFC) vs 조각 조립(NFD) | unicodedata.normalize("NFC", a) |
안 맞추면 같은 글자가 == 에서 False |
| 한 가지 모양으로 통일 | 비교 전에 NFC 로 정규화 | 한글 검색·파일명에서 단골 버그 |
한 문장 정의 — NFC 정규화는 겉보기 같지만 속이 다른 한글을 하나의 표준 모양으로 통일하는 것이고, 한글 비교·검색의 정확도를 지켜 줍니다.
예시 폭격
(1) 완성예 — 정규화 전후 비교
import unicodedata
a = "한글"
b = unicodedata.normalize("NFD", a) # 조각 조립 방식으로
print(a == b) # False (다르다고 나옴)
print(a == unicodedata.normalize("NFC", b)) # True (통일하니 같아짐)
NFC 로 맞추기 전에는 같은 "한글"인데도 다르다고 나옵니다.
(2) 부분완성 — 비교 전에 한 줄만 채우면 버그가 사라집니다.
import unicodedata
def same_word(x, y):
x = unicodedata.normalize("NFC", x)
y = unicodedata.____( "NFC", y) # 빈칸: normalize
return x == y
(3) 독립적용 — 검색어와 파일명을 비교하기 전에, 둘 다 NFC 로 정규화하는 함수를 직접 만들어 보세요.
미니 시나리오 — 맥과 윈도우 사이의 단골 버그
맥에서 만든 파일과 윈도우에서 만든 파일이 있습니다.
둘 다 이름이 "보고서"인데, 검색하면 한쪽이 안 잡힙니다.
맥은 조각 조립 방식(NFD), 윈도우는 도장 방식(NFC) 으로 저장하는 경우가 많기 때문입니다.
비교 전에 양쪽을 모두 NFC 로 정규화하면 깔끔하게 해결됩니다.
더 깊이 (지금 몰라도 됨) — Hugging Face LLM Course
정리
| 개념 | 한 줄 역할 |
|---|---|
| 토큰화·BPE | 문장을 조각으로 쪼개기 (비용·단위의 기준) |
| 임베딩 | 조각 번호를 의미 숫자 줄로 |
| 위치 표식 | 단어 순서를 알려 주기 |
| NFC 정규화 | 겉보기 같은 한글을 한 모양으로 통일 |
핵심 3줄로 —
AI 는 글자를 직접 못 읽어 숫자로 바꿉니다.
그 흐름이 "문장 → 토큰 → 번호 → 임베딩(+위치)" 입니다.
한글은 비교 전에 NFC 로 통일해야 같은 단어로 처리됩니다.
단순 규칙 — 글자를 다루기 전에 "쪼개고(토큰), 번호 붙이고(ID), 의미 줄로 바꾸고(임베딩), 순서 표시하고(위치), 한글이면 NFC 로 통일한다." 이 순서만 기억하면 됩니다.
한 걸음 더 ▸ (지금 몰라도 됨)
토큰·임베딩·위치 표식은 결국 0장의 세 갈래로 풀립니다. 숫자로 바꾸고(임베딩=벡터), 방향으로 비슷함을 재고(코사인 유사도), 나머지는 곱하고 더하기입니다. 멋진 이름에 겁먹지 않아도 됩니다.
다음 장 예고 — 지금까지 글자를 숫자로 바꾸는 입구를 봤습니다. (지금 몰라도 됩니다 — 다음 장에서 이어집니다.)
클릭하거나 Space를 눌러 뒤집기